import { Fragment, Node as ProseMirrorNode, NodeType, Slice } from "@tiptap/pm/model";
import { TextSelection } from "@tiptap/pm/state";
import { canSplit } from "@tiptap/pm/transform";
import { RawCommands } from "@tiptap/vue-3";
import { getNodeType } from "@tiptap/core";
import { getSplittedAttributes } from "@/extension";
import { getId } from "@/utils/id";

declare module "@tiptap/core" {
  interface Commands<ReturnType> {
    splitListItem: {
      /**
       * Splits one list item into two list items.
       */
      splitListItem: (typeOrName: string | NodeType) => ReturnType;
    };
  }
}

/**
 * 覆盖系统默认分割方法
 * @param typeOrName
 */
export const splitListItem: RawCommands["splitListItem"] =
  (typeOrName) =>
  ({ tr, state, dispatch, editor }) => {
    const type = getNodeType(typeOrName, state.schema);
    const { $from, $to } = state.selection;

    // eslint-disable-next-line @typescript-eslint/ban-ts-comment
    // @ts-ignore
    // eslint-disable-next-line
    const node: ProseMirrorNode = state.selection.node

    if ((node && node.isBlock) || $from.depth < 2 || !$from.sameParent($to)) {
      return false;
    }

    const grandParent = $from.node(-1);

    if (grandParent.type !== type) {
      return false;
    }

    const extensionAttributes = editor.extensionManager.attributes;

    if ($from.parent.content.size === 0 && $from.node(-1).childCount === $from.indexAfter(-1)) {
      // In an empty block. If this is a nested list, the wrapping
      // list item should be split. Otherwise, bail out and let next
      // command handle lifting.
      if ($from.depth === 2 || $from.node(-3).type !== type || $from.index(-2) !== $from.node(-2).childCount - 1) {
        return false;
      }

      if (dispatch) {
        let wrap = Fragment.empty;
        // eslint-disable-next-line
        const depthBefore = $from.index(-1) ? 1 : $from.index(-2) ? 2 : 3

        // Build a fragment containing empty versions of the structure
        // from the outer list item to the parent node of the cursor
        for (let d = $from.depth - depthBefore; d >= $from.depth - 3; d -= 1) {
          wrap = Fragment.from($from.node(d).copy(wrap));
        }

        // eslint-disable-next-line
        const depthAfter = $from.indexAfter(-1) < $from.node(-2).childCount ? 1 : $from.indexAfter(-2) < $from.node(-3).childCount ? 2 : 3

        // Add a second list item with an empty default start node
        const newNextTypeAttributes = getSplittedAttributes(extensionAttributes, $from.node().type.name, $from.node().attrs);
        const nextType = type.contentMatch.defaultType?.createAndFill(newNextTypeAttributes) || undefined;

        wrap = wrap.append(Fragment.from(type.createAndFill(null, nextType) || undefined));

        const start = $from.before($from.depth - (depthBefore - 1));

        tr.replace(start, $from.after(-depthAfter), new Slice(wrap, 4 - depthBefore, 0));

        let sel = -1;

        tr.doc.nodesBetween(start, tr.doc.content.size, (n, pos) => {
          if (sel > -1) {
            return false;
          }

          if (n.isTextblock && n.content.size === 0) {
            sel = pos + 1;
          }
        });

        if (sel > -1) {
          tr.setSelection(TextSelection.near(tr.doc.resolve(sel)));
        }

        tr.scrollIntoView();
      }

      return true;
    }

    const nextType = $to.pos === $from.end() ? grandParent.contentMatchAt(0).defaultType : null;
    const newTypeAttributes = getSplittedAttributes(extensionAttributes, grandParent.type.name, grandParent.attrs);
    const newNextTypeAttributes = getSplittedAttributes(extensionAttributes, $from.node().type.name, $from.node().attrs);
    if (newTypeAttributes.id) {
      newTypeAttributes.id = getId();
    }
    if (newNextTypeAttributes.id) {
      newNextTypeAttributes.id = getId();
    }
    tr.delete($from.pos, $to.pos);

    const types = nextType
      ? [
          { type, attrs: newTypeAttributes },
          { type: nextType, attrs: newNextTypeAttributes }
        ]
      : [{ type, attrs: newTypeAttributes }];

    if (!canSplit(tr.doc, $from.pos, 2)) {
      return false;
    }

    if (dispatch) {
      const { selection, storedMarks } = state;
      const { splittableMarks } = editor.extensionManager;
      const marks = storedMarks || (selection.$to.parentOffset && selection.$from.marks());

      tr.split($from.pos, 2, types).scrollIntoView();

      if (!marks || !dispatch) {
        return true;
      }

      const filteredMarks = marks.filter((mark) => splittableMarks.includes(mark.type.name));

      tr.ensureMarks(filteredMarks);
    }

    return true;
  };
